package main

import (
	"fmt"

	"github.com/urfave/cli/v2"
	"gopkg.in/ini.v1"
)

// The application version,
// which is dynamically injected by the makefile file,
// here it is just a declaration.
var Version = "0.0.1"

// The Key of the database connection string (aka. dsn).
const dataSourceKey = "data.source"

// Using urfave/cli for command line parameter parsing,
// it is worth noting that only one fixed parameter `dsn` is specified,
// and the rest of the participation is injected via dynamic methods.
var App = &cli.App{
	Name:    "kingbase_exporter",
	Usage:   "Kingbase server exporter.",
	Version: Version,
	Flags: []cli.Flag{
		&cli.StringFlag{
			Name:    dataSourceKey,
			Usage:   "The connect string for kingbase server.",
			EnvVars: []string{"DATA_SOURCE"},
		},
	},
}

// Definition of dynamic injection of generic command line parameters.
type GlobalFlag struct {
	// Command line parameters are grouped in order to fit ini files.
	// The full command line parameter names will be used ". " to connect the following two levels.
	firstLevelName  string
	secondLevelName string

	// Command line parameter description.
	usage string

	// Command line parameters required
	required bool

	// Default value for command line arguments,
	// currently only string and int types are supported.
	defaultVal interface{}
}

// Add generic command line parameters to the cli command parsing module.
// Called in the init magic function and executed before the main function.
func addGlobalFlags() {
	for _, v := range globalFlags {
		var flag cli.Flag

		switch val := v.defaultVal.(type) {
		case string:
			flag = &cli.StringFlag{
				Name:     v.firstLevelName + "." + v.secondLevelName,
				Usage:    v.usage,
				Required: v.required,
				Value:    val,
			}
		case int:
			flag = &cli.IntFlag{
				Name:     v.firstLevelName + "." + v.secondLevelName,
				Usage:    v.usage,
				Required: v.required,
				Value:    val,
			}
		default:
		}

		App.Flags = append(App.Flags, flag)
	}
}

// Add collector command line arguments to the cli command parsing module.
// Called in the init magic function and executed before the main function.
// All command line arguments are placed under the collector grouping.
func addCollectorFlags() {
	for k, v := range defaultCollectorFlags {
		App.Flags = append(App.Flags, &cli.BoolFlag{
			Name:  "collector." + k.Name(),
			Usage: k.Usage(),
			Value: v,
		})
	}
}

// Integration of configuration files and command line parameters
type Conf map[string]interface{}

// Initialize the default configuration into the map.
func (c *Conf) initConf() {
	(*c)[dataSourceKey] = ""

	for _, v := range globalFlags {
		fullName := v.firstLevelName + "." + v.secondLevelName
		(*c)[fullName] = v.defaultVal
	}

	for k, v := range defaultCollectorFlags {
		fullName := "collector." + k.Name()
		(*c)[fullName] = v
	}
}

// Load configuration from cli.
// The configuration passed in on the command line has a higher
// priority than the one in the file,
// so it will unconditionally overwrite the existing configuration.
func (c *Conf) loadConfigFromCmd(cCtx *cli.Context) error {
	// data source
	if cCtx.IsSet(dataSourceKey) && cCtx.String(dataSourceKey) != "" {
		(*c)[dataSourceKey] = cCtx.String(dataSourceKey)
	}

	// global flags
	for _, v := range globalFlags {
		fullName := v.firstLevelName + "." + v.secondLevelName

		switch v.defaultVal.(type) {
		case string:
			cfg := cCtx.String(fullName)
			if cCtx.IsSet(fullName) && cfg != "" {
				(*c)[fullName] = cfg
			}
		case int:
			cfg := cCtx.Int(fullName)
			if cCtx.IsSet(fullName) && cfg != 0 {
				(*c)[fullName] = cfg
			}
		}
	}

	// scraper flags
	for k := range defaultCollectorFlags {
		fullName := "collector." + k.Name()
		cfg := cCtx.Bool(fullName)
		if cCtx.IsSet(fullName) {
			(*c)[fullName] = cfg
		}
	}

	return nil
}

// Load config from file.
func (c *Conf) loadConfigFromFile(path string) error {
	cfg, err := ini.LoadSources(ini.LoadOptions{AllowBooleanKeys: true}, path)
	if err != nil {
		return err
	}

	// data source
	fileCfg := cfg.Section("data").Key("source").String()
	if fileCfg != "" {
		(*c)["data.source"] = fileCfg
	}

	// global flags
	for _, v := range globalFlags {
		fullName := v.firstLevelName + "." + v.secondLevelName

		switch v.defaultVal.(type) {
		case string:
			fileCfg := cfg.Section(v.firstLevelName).Key(v.secondLevelName).String()
			if fileCfg != "" {
				(*c)[fullName] = fileCfg
			}
		case int:
			fileCfg := cfg.Section(v.firstLevelName).Key(v.secondLevelName).MustInt()
			if fileCfg != 0 {
				(*c)[fullName] = fileCfg
			}
		}
	}

	// scraper flags
	for k, v := range defaultCollectorFlags {
		fullName := "collector." + k.Name()
		fileCfg := cfg.Section("collector").Key(k.Name()).MustBool()
		if fileCfg != v {
			(*c)[fullName] = fileCfg
		}
	}

	return nil
}

// Check the validity of the configuration, currently only checked if datasource is set.
// TODO: This item should be examined in more detail.
func (c *Conf) checkConf() error {
	if s, ok := (*c)[dataSourceKey].(string); !ok || (ok && s == "") {
		return fmt.Errorf("the data source is empty, it should be connect string")
	}

	return nil
}

// Initializes the configuration and loads it from the file when a config path is specified.
func (c *Conf) Init(ctx *cli.Context) error {
	c.initConf()

	// Load configuration from file.
	if path := ctx.String("config.source"); path != "" {
		if err := c.loadConfigFromFile(path); err != nil {
			return err
		}
	}

	if err := c.loadConfigFromCmd(ctx); err != nil {
		return err
	}

	if err := c.checkConf(); err != nil {
		return err
	}

	return nil
}

// Load the configuration of type string from the configuration map.
// If not configured will return the empty string.
func (c *Conf) MustString(name string) string {
	if val, ok := (*c)[name].(string); ok {
		return val
	}

	return ""
}

// Load the configuration of type int from the configuration map.
// If not configured will return zero.
func (c *Conf) MustInt(name string) int {
	if val, ok := (*c)[name].(int); ok {
		return val
	}

	return 0
}

// Load the configuration of type bool from the configuration map.
// If not configured will return false.
func (c *Conf) MustBool(name string) bool {
	if val, ok := (*c)[name].(bool); ok {
		return val
	}

	return false
}
